<?php

/**
 * Classe d'abstraction de connexion à la base de donnée
 */
class Database {

// ATTRIBUTS

    var $db;  // objet base de données (link)
    var $dbName;  // nom de la base de données
    var $serverName; // nom du serveur
    var $strict;  // si FALSE, on est autorisé à utilisé le failover

    /**
     * List of all executed queries (for debugging purpose)
     * @var array
     */
    public static $queries = array();

    /** Is database ready to server requests ? */
    private $isReady;

    /**
     * Utilisé par le factory
     * @var array
     */
    private static $connexionList = array();

    /**
     * Alias list
     * @var array
     */
    private static $databaseServers;

// Setters, Getters.
    public function isReady() {
        return $this->isReady;
    }

    private function setReady($val) {
        $this->isReady = $val;
    }

    /**
     * Factory de connexion à la base de donnée
     *
     * @param string $serverName server alias SYNTHESIO,UNITY,HOTEL,DATA,DBWRITE,DBREAD ...
     * @param string $dbName database name synthesio,crawl ...
     * @param bool $strict
     * @return Database
     */
    public static function factory($serverName = null) {

// if the class is called for the first time, we load the connection config
        self::loadConfig();
        
        if (!isset(self::$connexionList[$serverName]) || self::$connexionList[$serverName] instanceof Database == false) {
            $database = new Database($serverName);
            if ($database instanceOf Database && TRUE === $database->isReady()) {
                self::$connexionList[$serverName] = $database;
            } else {
                self::$connexionList[$serverName] = NULL;
            }
        }

        return self::$connexionList[$serverName];
    }

// CONSTRUCTEUR PROTECTED

    /**
     * Classe d'abstraction des bases de données
     *
     * Constructeur
     *
     * @param	string	$serverName	Nom du serveur auquel nous voulons nous connecter
     * @param	string	$dbName	Nom de la base de données à laquelle nous voulons nous connecter
     * @param	bool		$strict	si TRUE, la connection doit se faire STRICTEMENT sur le serveur demandé. SI FALSE (défaut), on accepte d'être connecté à un serveur de backup en cas d'échec (réplication)
     */
    protected function __construct($serverName, $strict = false) {

        $this->serverName = $serverName;
        $this->strict = $strict;

// Got any problem ? Is everything fine ?
        $this->setReady($this->Connect());
    }

    private function Connect($serverName = null) {

//$lstHost = self::$lstHost;
        $databaseServers = self::$databaseServers;

        if (empty($serverName)) {
// Use the current server name
            $serverName = $this->serverName;
        }

        if (!array_key_exists($serverName, $databaseServers)) {
            throw new Exception("Le serveur de base de données demandé n'existe pas : $serverName");
        }

        $connectInfos = $databaseServers[$serverName];

        if ($connectInfos["pass"] == "") {
            throw new Exception("Renseignez les informations de connection pour vous connecter à $serverName depuis ce serveur.");
        }
        try {
            //$dns = 'mysql:host=' . $connectInfos["server"] . ';dbname=' . $serverName;
            $dns = 'mysql:dbname=' . $serverName . ';host=' . $connectInfos["server"];
//echo $dns;
            $this->db = new PDO($dns, $connectInfos["user"], $connectInfos["pass"], array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
            $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->Query("SET NAMES utf8");
            $this->Query("SET time_zone = 'Europe/Paris';");
        } catch (Exception $e) {
            Log::pr("Connection à MySQL impossible : ", $e->getMessage());
        }
        return true;
    }

    /**
     * Ferme la connexion à la base
     */
    function Close() {

        if (empty($this->db)) {
            return;
        }

        unset(self::$connexionList[$this->serverName][$this->dbName][$this->strict]);

        $this->db = null;
    }

    protected function ReConnect($serverName = null) {
        $this->Close();
        $this->Connect($serverName);
    }

    public function Exec($sql) {
        $statement = false;
        $statement = $this->db->exec($sql);
        return $statement;
    }

    public function Prepare($sql) {
        $statement = false;
        $statement = $this->db->prepare($sql);
        return $statement;
    }

    public function Execute($sql, $arguments = array()) {
        $statement = false;
        $statement = $this->Prepare($sql);
        $statement->execute($arguments);
        return $statement;
    }

    /**
     * Retourne le résultat de mysql_query
     *
     * @param type $sql
     * @return ressource
     */
    public function Query($sql) {
        $sql = trim($sql);

        // On garde une trace de la requète
        self::$queries[] = $sql;

        $statement = false;
        // on execute la requete
        if (preg_match("~^(?:SELECT|SET|DESC)\s~i", $sql)) {
            try {
                $statement = $this->db->query($sql);
            } catch (Exception $e) {
                //Log::pr($e->getMessage());
            }
        } else {
            //Problème de construction de la requête
            Log::pr($sql, 'Erreur ! Pas de Select trouvé');
        }
        //la requête n'a pas fonctionné
        if (!$statement) {
            $error = $this->db->errorInfo();
//            error_log($error[2]);
        }
        return $statement;
    }

    /**
     * Retourne l'objet Result issu de mysql_query
     *
     * @param string $sql
     * @return Result
     */
    public function getResult($sql) {
        return new Result($this->Query($sql));
    }

//----------------------------------------------
//                  IsAlive()
//----------------------------------------------
    function IsAlive() {

        if (is_resource($this->db)) {
            return mysql_ping($this->db);
        }

        return false;
    }

    /**
     * Retourne le nombre de lignes affectées par le dernier UPDATE/DELETE/INSERT
     * @return int
     */
    public function getAffectedRows() {
        return mysql_affected_rows($this->db);
    }

//----------------------------------------------
//                 TableExists()
//----------------------------------------------
// informe si la table de nom @table existe ou pas
    function TableExists($table) {
        $sql = 'DESC ' . $this->serverName . '.' . $table . ';';
        return (bool) $this->Query($sql);
    }

    function DropTable($table) {
        if ($this->TableExists($table)) {
            return (bool) $this->Exec('DROP TABLE ' . $this->serverName . '.' . $table . ';');
        }
        return true;
    }

//----------------------------------------------
//                    LastId()
//----------------------------------------------
    public function LastId() {
        return $this->db->lastInsertId();
    }

    protected function HandleError($msg, $thenExit = false) {

        $msg = trim($msg);
        $textMsg = $msg;

        if (php_sapi_name() == 'cli') {
            $lineFeed = "\n";
            $context = $_SERVER['SCRIPT_NAME'];
        } else {
            $lineFeed = "<br />";
            $textMsg = nl2br($textMsg);
            $context = $_SERVER['REQUEST_URI'] . ' ' . $_SERVER['REMOTE_ADDR'];
        }

        if (defined('DEBUG_MODE') && DEBUG_MODE) {
            echo $textMsg, $lineFeed;
        }

        if (function_exists('save_to_log')) {
            save_to_log("\n$msg", "errors");
        }

        $msg = preg_replace('~\s+~', " ", $msg);
        error_log('DbError: ' . $msg . ' ' . $context);

        if ($thenExit) {
            exit();
        }
    }

    /**
     * Attention, surchargé dans Db
     */
    protected function HandleCommandDenied($sql) {
        var_dump("COMMAND_DENIED $sql");
        return false;
    }

    /**
     * Return a formatted string SQL
     * @link http://php.net/manual/en/function.sprintf.php
     * @param string $format
     * @param mixed $args [optional]
     * @param mixed $_ [optional]
     * @return string a string produced according to the formatting string
     * format.
     */
    public static function sprintf($sql, $args = null, $_ = null) {

        $parms = func_get_args();

        array_shift($parms); // On vire le premier paramètre $sql

        $escapedParms = array_map(array(__CLASS__, 'escape'), $parms);

        $res = @vsprintf($sql, $escapedParms);

        if ($res === false) {
            throw new DatabaseException(__METHOD__ . ' - Arguments problems');
        }

        return $res;
    }

    /**
     * S'utilise pour escaper une variable
     *
     * Dans la plupart des cas vous devriez utiliser escape
     *
     * exemple
     *  chaine vide                   => chaine vide
     *  0                             => 0
     *  1                             => 1
     *  1,4                           => 1,4
     *  1.5                           => 1.5
     *  true                          => 1
     *  NULL                          => NULL
     *  false                         => 0
     *  array(1, 2)                   => DatabaseException escapeWithoutQuotes do not accept array
     *  array('O', 'C')               => DatabaseException escapeWithoutQuotes do not accept array
     *  test                          => test
     *  c'est la fete                 => c\'est la fete
     *  Object                        => DatabaseException escapeWithoutQuotes do not accept object
     *
     * @param mixed $param
     * @return string
     */
    public function escapeWithoutQuotes($param) {

        if (is_array($param) || is_object($param) || is_resource($param)) {
            throw new DatabaseException(__FUNCTION__ . ' do not accept ' . gettype($param));
        }

        if (is_bool($param) || is_numeric($param) && !is_float($param) && (substr($param, 0, 1) != '0' && strlen($param) > 1)) {
            return (string) (int) $param;  // On caste en string car on veut le rajouter dans du texte
        }

        if (is_null($param)) {
            return 'NULL';
        }

        if (strtolower($param) == 'now()') {
            return $param;
        }

        if (!strlen($param)) {
            return '';
        }

        $db = self::factory(DEBUG ? 'front' : 'certinerform');
        $escaped = $db->db->quote($param, PDO::PARAM_STR);

        return $escaped;
    }

    public function quoteNull($value) {
        if (is_null($value)) {
            return $this->db->quote($value, PDO::PARAM_NULL); //'NULL';
        } else {
            return $this->db->quote($value);
        }
    }

    /**
     * S'utilise pour escaper une variable, un tableau, rajoute des quotes
     *
     *  exemples
     *  chaine vide                   => chaine vide
     *  0                             => 0
     *  1                             => 1
     *  1,4                           => '1,4'
     *  1.5                           => '1.5'
     *  true                          => 1
     *  NULL                          => NULL
     *  false                         => 0
     *  array(1, 2)                   => 1,2
     *  array('O', 'C')               => 'O','C'
     *  test                          => 'test'
     *  c'est la fete                 => 'c\'est la fete'
     *  Object                        => DatabaseException escape do not accept object
     *
     * @param mixed $values
     * @return string
     */
    public static function escape($param) {

        if (!is_array($param)) {
            $param = array($param);
        }

        $valuesEscaped = array_map(array(__CLASS__, 'escapeSQLWithQuotesCallback'), $param);

        return implode(',', $valuesEscaped);
    }

    private static function escapeSQLWithQuotesCallback($param) {

        if (is_null($param)) {
            return 'NULL';
        }

        $escaped = self::escapeWithoutQuotes($param);

        if (is_numeric($escaped) && !is_float($param)) {
            return $escaped;
        }

        if (strlen($escaped) == 0) {
            return "''";
        }

        return sprintf("%s", $escaped);
    }

    /**
     * Retourne une chaine regex Mysql
     *
     * Voir http://dev.mysql.com/doc/refman/5.1/en/regexp.html
     *
     * @param type $values
     * @return type
     */
    public static function getTagRegexSQL($values) {

        if (!is_array($values)) {
            $values = array($values);
        }

        $escaped = array_map(array(__CLASS__, 'escapeTonalizedTagCallback'), $values);

        return self::escapeSQLWithQuotesCallback(sprintf("[[:<:]](%s)", implode('|', $escaped)));
    }

    private static function escapeTonalizedTagCallback($tag) {

        $tagEscaped = self::escapeWithoutQuotes($tag);

        if (preg_match('#[\-\+\=~,]$#', $tagEscaped)) {

// NB on utilise la notation regex [[.hyphen-minus.]] car [.-.] ne fonctionne pas
// Attention le remplacement de "-" doit être fait en premier sans il y aura un remplacement des signes - présents dans les commandes
            $tagEscaped = str_replace(array('-', '+', '=', '~', ','), array('[[.hyphen-minus.]]', '[[.plus-sign.]]', '[[.equals-sign.]]', '[[.tilde.]]', ','), $tagEscaped);
        } else {
            $tagEscaped .= '[[:>:]]';
        }

        return $tagEscaped;
    }

    static function loadConfig() {
        if (!self::$databaseServers) {
            $config = DBConfig::getConfig();
            self::$databaseServers = $config['databaseServers'];
        }
    }

    /**
     * Renvoie toutes les requêtes exécutées par l'objet
     */
    public static function getQueries() {
        return self::$queries;
    }

}

class DatabaseException extends Exception {
    
}

?>
